package com.isseiaoki.simplecropview;


import com.isseiaoki.simplecropview.animation.SimpleValueAnimator;
import com.isseiaoki.simplecropview.animation.SimpleValueAnimatorListener;
import com.isseiaoki.simplecropview.animation.ValueAnimatorV14;
import com.isseiaoki.simplecropview.animation.ValueAnimatorV8;
import com.isseiaoki.simplecropview.callback.Callback;
import com.isseiaoki.simplecropview.callback.CropCallback;
import com.isseiaoki.simplecropview.callback.LoadCallback;
import com.isseiaoki.simplecropview.callback.SaveCallback;
import com.isseiaoki.simplecropview.util.*;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.annotations.NonNull;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.Image;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.render.*;
import ohos.agp.utils.*;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.global.resource.Resource;
import ohos.global.resource.ResourceManager;
import ohos.media.image.ImagePacker;
import ohos.media.image.ImageSource;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.net.Uri;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

public class CropImageView extends Image implements Component.DrawTask, Component.TouchEventListener, Component.EstimateSizeListener {
    private static final String TAG = CropImageView.class.getSimpleName();

    // Constants ///////////////////////////////////////////////////////////////////////////////////

    private static final int HANDLE_SIZE_IN_DP = 14;
    private static final int MIN_FRAME_SIZE_IN_DP = 50;
    private static final int FRAME_STROKE_WEIGHT_IN_DP = 1;
    private static final int GUIDE_STROKE_WEIGHT_IN_DP = 1;
    private static final float DEFAULT_INITIAL_FRAME_SCALE = 1f;
    private static final int DEFAULT_ANIMATION_DURATION_MILLIS = 100;
    private static final int DEBUG_TEXT_SIZE_IN_DP = 15;

    private static final int TRANSPARENT = 0x00000000;
    private static final int TRANSLUCENT_WHITE = 0xBBFFFFFF;
    private static final int WHITE = 0xFFFFFFFF;
    private static final int TRANSLUCENT_BLACK = 0xBB000000;

    // Member variables ////////////////////////////////////////////////////////////////////////////

    private int mViewWidth = 0;
    private int mViewHeight = 0;
    private float mScale = 1.0f;
    private float mAngle = 0.0f;
    private float mImgWidth = 0.0f;
    private float mImgHeight = 0.0f;

    private boolean mIsInitialized = false;
    private Matrix mMatrix = null;
    private Paint mPaintTranslucent;
    private Paint mPaintFrame;
    private Paint mPaintBitmap;
    private Paint mPaintDebug;
    private RectF mFrameRect;
    private RectF mInitialFrameRect;
    private RectF mImageRect;
    private PointF mCenter = new PointF();
    private float mLastX, mLastY;
    private boolean mIsRotating = false;
    private boolean mIsAnimating = false;
    private SimpleValueAnimator mAnimator = null;
    private EventHandler mHandler = new EventHandler(EventRunner.create());
    private Uri mSourceUri = null;
    private Uri mSaveUri = null;
    private int mExifRotation = 0;
    private int mOutputMaxWidth;
    private int mOutputMaxHeight;
    private int mOutputWidth = 0;
    private int mOutputHeight = 0;
    private boolean mIsDebug = false;
    private int mCompressQuality = 100;
    private int mInputImageWidth = 0;
    private int mInputImageHeight = 0;
    private int mOutputImageWidth = 0;
    private int mOutputImageHeight = 0;
    private AtomicBoolean mIsLoading = new AtomicBoolean(false);
    private AtomicBoolean mIsCropping = new AtomicBoolean(false);
    private AtomicBoolean mIsSaving = new AtomicBoolean(false);
    private ExecutorService mExecutor;

    private TouchArea mTouchArea = TouchArea.OUT_OF_BOUNDS;

    private CropMode mCropMode = CropMode.SQUARE;
    private ShowMode mGuideShowMode = ShowMode.SHOW_ALWAYS;
    private ShowMode mHandleShowMode = ShowMode.SHOW_ALWAYS;
    private float mMinFrameSize;
    private int mHandleSize;
    private int mTouchPadding = 0;
    private boolean mShowGuide = true;
    private boolean mShowHandle = true;
    private boolean mIsCropEnabled = true;
    private boolean mIsEnabled = true;
    private PointF mCustomRatio = new PointF(1.0f, 1.0f);
    private float mFrameStrokeWeight = 2.0f;
    private float mGuideStrokeWeight = 2.0f;
    private int mBackgroundColor;
    private int mOverlayColor;
    private int mFrameColor;
    private int mHandleColor;
    private int mGuideColor;
    private float mInitialFrameScale; // 0.01 ~ 1.0, 0.75 is default value
    private boolean mIsAnimationEnabled = true;
    private int mAnimationDurationMillis = DEFAULT_ANIMATION_DURATION_MILLIS;
    private boolean mIsHandleShadowEnabled = true;

    private static final String scv_img_src = "scv_img_src";
    private static final String scv_crop_mode = "scv_crop_mode";
    private static final String scv_background_color = "scv_background_color";
    private static final String scv_overlay_color = "scv_overlay_color";
    private static final String scv_frame_color = "scv_frame_color";
    private static final String scv_handle_color = "scv_handle_color";
    private static final String scv_guide_color = "scv_guide_color";
    private static final String scv_guide_show_mode = "scv_guide_show_mode";
    private static final String scv_handle_show_mode = "scv_handle_show_mode";
    private static final String scv_min_frame_size = "scv_min_frame_size";
    private static final String scv_touch_padding = "scv_touch_padding";
    private static final String scv_frame_stroke_weight = "scv_frame_stroke_weight";
    private static final String scv_guide_stroke_weight = "scv_guide_stroke_weight";
    private static final String scv_crop_enabled = "scv_crop_enabled";
    private static final String scv_initial_frame_scale = "scv_initial_frame_scale";
    private static final String scv_animation_enabled = "scv_animation_enabled";
    private static final String scv_animation_duration = "scv_animation_duration";
    private static final String scv_handle_shadow_enabled = "scv_handle_shadow_enabled";
    private static final String scv_handle_size = "scv_handle_size";

    // Constructor /////////////////////////////////////////////////////////////////////////////////

    public CropImageView(Context context, AttrSet attrs) {
        this(context, attrs, null);
    }

    public CropImageView(Context context, AttrSet attrs, String defStyle) {
        super(context, attrs, defStyle);

        mExecutor = Executors.newSingleThreadExecutor();

        float density = getDensity();
        mHandleSize = (int) (density * HANDLE_SIZE_IN_DP);
        mMinFrameSize = density * MIN_FRAME_SIZE_IN_DP;
        mFrameStrokeWeight = density * FRAME_STROKE_WEIGHT_IN_DP;
        mGuideStrokeWeight = density * GUIDE_STROKE_WEIGHT_IN_DP;

        mPaintFrame = new Paint();
        mPaintTranslucent = new Paint();
        mPaintBitmap = new Paint();
        mPaintBitmap.setFilterBitmap(true);
        mPaintDebug = new Paint();
        mPaintDebug.setAntiAlias(true);
        mPaintDebug.setStyle(Paint.Style.STROKE_STYLE);
        mPaintDebug.setColor(new Color(WHITE));
        mPaintDebug.setTextSize((int) (DEBUG_TEXT_SIZE_IN_DP * density));

        mMatrix = new Matrix();
        mScale = 1.0f;
        mBackgroundColor = TRANSPARENT;
        mFrameColor = WHITE;
        mOverlayColor = TRANSLUCENT_BLACK;
        mHandleColor = WHITE;
        mGuideColor = TRANSLUCENT_WHITE;

        // handle Styleable
        handleStyleable(attrs);
        addDrawTask(this);
        setTouchEventListener(this);
        setEstimateSizeListener(this);
    }

    // Handle styleable ////////////////////////////////////////////////////////////////////////////

    private void handleStyleable(AttrSet attrs) {
        mCropMode = CropMode.CIRCLE;
        Element drawable = null;
        boolean isPresentScvImgSrc = attrs.getAttr(scv_img_src).isPresent();
        if (isPresentScvImgSrc) {
            drawable = attrs.getAttr(scv_img_src).get().getElement();
        }
        if (drawable != null) {
            setImageSrc(drawable);
        }
        boolean isPresentScvCropMode = attrs.getAttr(scv_crop_mode).isPresent();
        if (isPresentScvCropMode) {
            for (CropMode mode : CropMode.values()) {
                if (getCropMode(attrs.getAttr(scv_crop_mode).get().getStringValue()) == mode.getId()) {
                    mCropMode = mode;
                    break;
                }
            }
        }

        boolean isPresentScvBgColor = attrs.getAttr(scv_background_color).isPresent();
        if (isPresentScvBgColor) {
            mBackgroundColor = attrs.getAttr(scv_background_color).get().getColorValue().getValue();
        }

        if (attrs.getAttr(scv_overlay_color).isPresent()) {
            mOverlayColor = attrs.getAttr(scv_overlay_color).get().getColorValue().getValue();
        }

        if (attrs.getAttr(scv_frame_color).isPresent()) {
            mFrameColor = attrs.getAttr(scv_frame_color).get().getColorValue().getValue();
        }

        if (attrs.getAttr(scv_handle_color).isPresent()) {
            mHandleColor = attrs.getAttr(scv_handle_color).get().getColorValue().getValue();
        }

        if (attrs.getAttr(scv_guide_color).isPresent()) {
            mGuideColor = attrs.getAttr(scv_guide_color).get().getColorValue().getValue();
        }

        if (attrs.getAttr(scv_guide_show_mode).isPresent()) {
            for (ShowMode mode : ShowMode.values()) {
                if (getHandleShowMode(attrs.getAttr(scv_guide_show_mode).get().getStringValue()) == mode.getId()) {
                    mGuideShowMode = mode;
                    break;
                }
            }
        }
        if (attrs.getAttr(scv_handle_show_mode).isPresent()) {
            for (ShowMode mode : ShowMode.values()) {
                if (getHandleShowMode(attrs.getAttr(scv_handle_show_mode).get().getStringValue()) == mode.getId()) {
                    mHandleShowMode = mode;
                    break;
                }
            }
        }

        setGuideShowMode(mGuideShowMode);
        setHandleShowMode(mHandleShowMode);

        if (attrs.getAttr(scv_handle_show_mode).isPresent()) {
            mHandleSize = attrs.getAttr(scv_handle_size).get().getDimensionValue();
        }

        if (attrs.getAttr(scv_touch_padding).isPresent()) {
            mTouchPadding = attrs.getAttr(scv_touch_padding).get().getDimensionValue();
        }

        if (attrs.getAttr(scv_min_frame_size).isPresent()) {
            mMinFrameSize = attrs.getAttr(scv_min_frame_size).get().getDimensionValue();
        }

        if (attrs.getAttr(scv_frame_stroke_weight).isPresent()) {
            mFrameStrokeWeight = attrs.getAttr(scv_frame_stroke_weight).get().getDimensionValue();
        }

        if (attrs.getAttr(scv_guide_stroke_weight).isPresent()) {
            mGuideStrokeWeight = attrs.getAttr(scv_guide_stroke_weight).get().getDimensionValue();
        }

        if (attrs.getAttr(scv_crop_enabled).isPresent()) {
            mIsCropEnabled = attrs.getAttr(scv_crop_enabled).get().getBoolValue();
        }

        if (attrs.getAttr(scv_initial_frame_scale).isPresent()) {
            mInitialFrameScale = constrain(attrs.getAttr(scv_initial_frame_scale).get().getFloatValue(), 0.01f, 1.0f, DEFAULT_INITIAL_FRAME_SCALE);
        }

        if (attrs.getAttr(scv_animation_enabled).isPresent()) {
            mIsAnimationEnabled = attrs.getAttr(scv_animation_enabled).get().getBoolValue();
        }

        if (attrs.getAttr(scv_animation_duration).isPresent()) {
            mAnimationDurationMillis = attrs.getAttr(scv_animation_duration).get().getIntegerValue();
        }

        if (attrs.getAttr(scv_handle_shadow_enabled).isPresent()) {
            mIsHandleShadowEnabled = attrs.getAttr(scv_handle_shadow_enabled).get().getBoolValue();
        }


    }

    private void setImageSrcByResource(Resource resource) {
        if (resource != null) {
            try {
                File file = new File(getContext().getFilesDir() + "/cropimageview_temp.png");
                if (!file.exists()) {
                    file.createNewFile();
                }
                resourceToFile(file, resource);
                Uri outputUri = fileToUri(file);
                PixelMap pixelMap = Utils.decodeSampledBitmapFromUri(getContext(), outputUri, 0);
                setPixelMap(pixelMap);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void setImageSrc(Element element) {
        if (element != null) {
            try {
                PixelMap pixelMap = ((PixelMapElement) element).getPixelMap();
                File file = new File(getContext().getFilesDir() + "/cropimageview_temp.png");
                if (!file.exists()) {
                    file.createNewFile();
                }
                pixmapToFile(file, pixelMap);
                Uri outputUri = fileToUri(file);
                pixelMap = Utils.decodeSampledBitmapFromUri(getContext(), outputUri, 0);
                setPixelMap(pixelMap);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void resourceToFile(File file, Resource resource) {
        try {
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));
            ImageSource.SourceOptions sourceOptions = new ImageSource.SourceOptions();
            sourceOptions.formatHint = "image/png";
            ImageSource imageSource = ImageSource.create(resource, sourceOptions);
            ImagePacker imagePacker = ImagePacker.create();
            ImagePacker.PackingOptions packingOptions = new ImagePacker.PackingOptions();
            packingOptions.format = "image/jpeg";
            packingOptions.quality = 90;
            imagePacker.initializePacking(bos, packingOptions);
            imagePacker.addImage(imageSource);
            imagePacker.finalizePacking();
            bos.flush();
            bos.close();
            imagePacker.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void pixmapToFile(File file, PixelMap pixelMap) {
        try {
            OutputStream outputStream = new FileOutputStream(file.getPath());
            ImagePacker imagePacker = ImagePacker.create();
            ImagePacker.PackingOptions packingOptions = new ImagePacker.PackingOptions();
            packingOptions.format = "image/jpeg";
            packingOptions.quality = 90;
            imagePacker.initializePacking(outputStream, packingOptions);
            imagePacker.addImage(pixelMap);
            imagePacker.finalizePacking();
            outputStream.flush();
            outputStream.close();
            imagePacker.release();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Uri fileToUri(File file) {
        return Uri.getUriFromFile(file);
    }


    public int getHandleShowMode(String mode) {
        switch (mode) {
            case "show_always":
                return 1;
            case "show_on_touch":
                return 2;
            case "not_show":
                return 3;
        }
        return 1;
    }

    public int getCropMode(String mode) {
        switch (mode) {
            case "fit_image":
                return 0;
            case "ratio_4_3":
                return 1;
            case "ratio_3_4":
                return 2;
            case "square":
                return 3;
            case "ratio_16_9":
                return 4;
            case "ratio_9_16":
                return 5;
            case "free":
                return 6;
            case "custom":
                return 7;
            case "circle":
                return 8;
            case "circle_square":
                return 9;
        }
        return 1;
    }


    // Drawing method //////////////////////////////////////////////////////////////////////////////

    private void drawCropFrame(Canvas canvas) {
        if (!mIsCropEnabled) return;
        if (mIsRotating) return;
        drawOverlay(canvas);
        drawFrame(canvas);
        if (mShowGuide) drawGuidelines(canvas);
        if (mShowHandle) drawHandles(canvas);
    }


    private void drawOverlay(Canvas canvas) {
        mPaintTranslucent.setAntiAlias(true);
        mPaintTranslucent.setFilterBitmap(true);
        mPaintTranslucent.setColor(new Color(mOverlayColor));
        mPaintTranslucent.setStyle(Paint.Style.FILL_STYLE);
        Path path = new Path();
        RectF overlayRect =
                new RectF((float) Math.floor(mImageRect.left), (float) Math.floor(mImageRect.top),
                        (float) Math.ceil(mImageRect.right), (float) Math.ceil(mImageRect.bottom));
        if (!mIsAnimating && (mCropMode == CropMode.CIRCLE || mCropMode == CropMode.CIRCLE_SQUARE)) {
            path.addRect(overlayRect, Path.Direction.CLOCK_WISE);
            PointF circleCenter = new PointF((mFrameRect.left + mFrameRect.right) / 2,
                    (mFrameRect.top + mFrameRect.bottom) / 2);
            float circleRadius = (mFrameRect.right - mFrameRect.left) / 2;
            path.addCircle(circleCenter.x, circleCenter.y, circleRadius, Path.Direction.COUNTER_CLOCK_WISE);
            canvas.drawPath(path, mPaintTranslucent);
        } else {
            path.addRect(overlayRect, Path.Direction.CLOCK_WISE);
            path.addRect(mFrameRect, Path.Direction.COUNTER_CLOCK_WISE);
            canvas.drawPath(path, mPaintTranslucent);
        }
    }

    private void drawFrame(Canvas canvas) {
        mPaintFrame.setAntiAlias(true);
        mPaintFrame.setFilterBitmap(true);
        mPaintFrame.setStyle(Paint.Style.STROKE_STYLE);
        mPaintFrame.setColor(new Color(mFrameColor));
        mPaintFrame.setStrokeWidth(mFrameStrokeWeight);
        canvas.drawRect(mFrameRect, mPaintFrame);
    }

    private void drawGuidelines(Canvas canvas) {
        mPaintFrame.setColor(new Color(mGuideColor));
        mPaintFrame.setStrokeWidth(mGuideStrokeWeight);
        float h1 = mFrameRect.left + (mFrameRect.right - mFrameRect.left) / 3.0f;
        float h2 = mFrameRect.right - (mFrameRect.right - mFrameRect.left) / 3.0f;
        float v1 = mFrameRect.top + (mFrameRect.bottom - mFrameRect.top) / 3.0f;
        float v2 = mFrameRect.bottom - (mFrameRect.bottom - mFrameRect.top) / 3.0f;
        canvas.drawLine(h1, mFrameRect.top, h1, mFrameRect.bottom, mPaintFrame);
        canvas.drawLine(h2, mFrameRect.top, h2, mFrameRect.bottom, mPaintFrame);
        canvas.drawLine(mFrameRect.left, v1, mFrameRect.right, v1, mPaintFrame);
        canvas.drawLine(mFrameRect.left, v2, mFrameRect.right, v2, mPaintFrame);
    }

    private void drawHandles(Canvas canvas) {
        if (mIsHandleShadowEnabled) drawHandleShadows(canvas);
        mPaintFrame.setStyle(Paint.Style.FILL_STYLE);
        mPaintFrame.setColor(new Color(mHandleColor));
        canvas.drawCircle(mFrameRect.left, mFrameRect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(mFrameRect.right, mFrameRect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(mFrameRect.left, mFrameRect.bottom, mHandleSize, mPaintFrame);
        canvas.drawCircle(mFrameRect.right, mFrameRect.bottom, mHandleSize, mPaintFrame);
    }

    private void drawHandleShadows(Canvas canvas) {
        mPaintFrame.setStyle(Paint.Style.FILL_STYLE);
        mPaintFrame.setColor(new Color(TRANSLUCENT_BLACK));
        RectF rect = new RectF();
        rect.left = mFrameRect.left;
        rect.top = mFrameRect.top;
        rect.bottom = mFrameRect.bottom;
        rect.right = mFrameRect.right;
        rect.offset(0, 1);
        canvas.drawCircle(rect.left, rect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.right, rect.top, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.left, rect.bottom, mHandleSize, mPaintFrame);
        canvas.drawCircle(rect.right, rect.bottom, mHandleSize, mPaintFrame);
    }

    private void setMatrix() {
        mMatrix.reset();
        mMatrix.setTranslate(mCenter.x - mImgWidth * 0.5f, mCenter.y - mImgHeight * 0.5f);
        mMatrix.postScale(mScale, mScale, mCenter.x, mCenter.y);
        mMatrix.postRotate(mAngle, mCenter.x, mCenter.y);
    }

    // Layout calculation //////////////////////////////////////////////////////////////////////////

    private void setupLayout(int viewW, int viewH) {
        if (viewW == 0 || viewH == 0) return;
        setCenter(new PointF(getPaddingLeft() + viewW * 0.5f, getPaddingTop() + viewH * 0.5f));
        setScale(calcScale(viewW, viewH, mAngle));
        setMatrix();
        mImageRect = calcImageRect(new RectF(0f, 0f, mImgWidth, mImgHeight), mMatrix);

        if (mInitialFrameRect != null) {
            mFrameRect = applyInitialFrameRect(mInitialFrameRect);
        } else {
            mFrameRect = calcFrameRect(mImageRect);
        }

        mIsInitialized = true;
        invalidate();
    }

    private float calcScale(int viewW, int viewH, float angle) {
        mImgWidth = -1;
        mImgHeight = -1;
        if (mImgWidth <= 0) mImgWidth = viewW;
        if (mImgHeight <= 0) mImgHeight = viewH;
        float viewRatio = (float) viewW / (float) viewH;
        float imgRatio = getRotatedWidth(angle) / getRotatedHeight(angle);
        float scale = 1.0f;
        if (imgRatio >= viewRatio) {
            scale = viewW / getRotatedWidth(angle);
        } else if (imgRatio < viewRatio) {
            scale = viewH / getRotatedHeight(angle);
        }
        return scale;
    }

    private RectF calcImageRect(RectF rect, Matrix matrix) {
        RectF applied = new RectF();
        matrix.mapRect(applied, rect);
        return applied;
    }

    private RectF calcFrameRect(RectF imageRect) {
        if (mInitialFrameScale <= 0) {
            mInitialFrameScale = 1.0f;
        }
        float frameW = getRatioX(imageRect.width());
        float frameH = getRatioY(imageRect.height());
        float imgRatio = imageRect.width() / imageRect.height();
        float frameRatio = frameW / frameH;
        float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom;
        if (frameRatio >= imgRatio) {
            l = imageRect.left;
            r = imageRect.right;
            float hy = (imageRect.top + imageRect.bottom) * 0.5f;
            float hh = (imageRect.width() / frameRatio) * 0.5f;
            t = hy - hh;
            b = hy + hh;
        } else if (frameRatio < imgRatio) {
            t = imageRect.top;
            b = imageRect.bottom;
            float hx = (imageRect.left + imageRect.right) * 0.5f;
            float hw = imageRect.height() * frameRatio * 0.5f;
            l = hx - hw;
            r = hx + hw;
        }
        float w = r - l;
        float h = b - t;
        float cx = l + w / 2;
        float cy = t + h / 2;
        float sw = w * mInitialFrameScale;
        float sh = h * mInitialFrameScale;
        return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2);
    }

    // Touch Event /////////////////////////////////////////////////////////////////////////////////


    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (!mIsInitialized) return true;
        if (!mIsCropEnabled) return true;
        if (!mIsEnabled) return true;
        if (mIsRotating) return true;
        if (mIsLoading.get()) return true;
        if (mIsCropping.get()) return true;

        switch (event.getAction()) {
            case TouchEvent.PRIMARY_POINT_DOWN:
                onDown(event);
                return true;
            case TouchEvent.POINT_MOVE:
                onMove(event);
                return true;
            case TouchEvent.CANCEL:
                onCancel();
                return true;
            case TouchEvent.PRIMARY_POINT_UP:
                onUp();
                return true;
        }
        return true;
    }

    private float getTouchX(TouchEvent touchEvent, int index) {
        float touchX = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                touchX = touchEvent.getPointerScreenPosition(index).getX() - xy[0];
            } else {
                touchX = touchEvent.getPointerPosition(index).getX();
            }
        }
        return touchX;
    }

    private float getTouchY(TouchEvent touchEvent, int index) {
        float touchY = 0;
        if (touchEvent.getPointerCount() > index) {
            int[] xy = getLocationOnScreen();
            if (xy != null && xy.length == 2) {
                touchY = touchEvent.getPointerScreenPosition(index).getY() - xy[1];
            } else {
                touchY = touchEvent.getPointerPosition(index).getY();
            }
        }
        return touchY;
    }

    private void onDown(TouchEvent e) {
        invalidate();
        mLastX = getTouchX(e, 0);
        mLastY = getTouchY(e, 0);
        checkTouchArea(mLastX, mLastY);
    }

    private void onMove(TouchEvent e) {
        float diffX = getTouchX(e, 0) - mLastX;
        float diffY = getTouchY(e, 0) - mLastY;
        switch (mTouchArea) {
            case CENTER:
                moveFrame(diffX, diffY);
                break;
            case LEFT_TOP:
                moveHandleLT(diffX, diffY);
                break;
            case RIGHT_TOP:
                moveHandleRT(diffX, diffY);
                break;
            case LEFT_BOTTOM:
                moveHandleLB(diffX, diffY);
                break;
            case RIGHT_BOTTOM:
                moveHandleRB(diffX, diffY);
                break;
            case OUT_OF_BOUNDS:
                break;
        }
        invalidate();
        mLastX = getTouchX(e, 0);
        mLastY = getTouchY(e, 0);
    }

    private void onUp() {
        if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = false;
        if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = false;
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
        invalidate();
    }

    private void onCancel() {
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
        invalidate();
    }

    // Hit test ////////////////////////////////////////////////////////////////////////////////////

    private void checkTouchArea(float x, float y) {
        if (isInsideCornerLeftTop(x, y)) {
            mTouchArea = TouchArea.LEFT_TOP;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerRightTop(x, y)) {
            mTouchArea = TouchArea.RIGHT_TOP;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerLeftBottom(x, y)) {
            mTouchArea = TouchArea.LEFT_BOTTOM;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideCornerRightBottom(x, y)) {
            mTouchArea = TouchArea.RIGHT_BOTTOM;
            if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true;
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            return;
        }
        if (isInsideFrame(x, y)) {
            if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true;
            mTouchArea = TouchArea.CENTER;
            return;
        }
        mTouchArea = TouchArea.OUT_OF_BOUNDS;
    }

    private boolean isInsideFrame(float x, float y) {
        if (mFrameRect.left <= x && mFrameRect.right >= x) {
            if (mFrameRect.top <= y && mFrameRect.bottom >= y) {
                mTouchArea = TouchArea.CENTER;
                return true;
            }
        }
        return false;
    }

    private boolean isInsideCornerLeftTop(float x, float y) {
        float dx = x - mFrameRect.left;
        float dy = y - mFrameRect.top;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerRightTop(float x, float y) {
        float dx = x - mFrameRect.right;
        float dy = y - mFrameRect.top;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerLeftBottom(float x, float y) {
        float dx = x - mFrameRect.left;
        float dy = y - mFrameRect.bottom;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    private boolean isInsideCornerRightBottom(float x, float y) {
        float dx = x - mFrameRect.right;
        float dy = y - mFrameRect.bottom;
        float d = dx * dx + dy * dy;
        return sq(mHandleSize + mTouchPadding) >= d;
    }

    // Adjust frame ////////////////////////////////////////////////////////////////////////////////

    private void moveFrame(float x, float y) {
        mFrameRect.left += x;
        mFrameRect.right += x;
        mFrameRect.top += y;
        mFrameRect.bottom += y;
        checkMoveBounds();
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleLT(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.left += diffX;
            mFrameRect.top += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.left += dx;
            mFrameRect.top += dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.left -= offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.left)) {
                ox = mImageRect.left - mFrameRect.left;
                mFrameRect.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.top += oy;
            }
            if (!isInsideVertical(mFrameRect.top)) {
                oy = mImageRect.top - mFrameRect.top;
                mFrameRect.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.left += ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleRT(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.right += diffX;
            mFrameRect.top += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.right += dx;
            mFrameRect.top -= dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.top -= offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.top -= offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.right += offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.right)) {
                ox = mFrameRect.right - mImageRect.right;
                mFrameRect.right -= ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.top += oy;
            }
            if (!isInsideVertical(mFrameRect.top)) {
                oy = mImageRect.top - mFrameRect.top;
                mFrameRect.top += oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.right -= ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleLB(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.left += diffX;
            mFrameRect.bottom += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.left += dx;
            mFrameRect.bottom -= dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.left -= offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.bottom += offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.left -= offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.left)) {
                ox = mImageRect.left - mFrameRect.left;
                mFrameRect.left += ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.bottom -= oy;
            }
            if (!isInsideVertical(mFrameRect.bottom)) {
                oy = mFrameRect.bottom - mImageRect.bottom;
                mFrameRect.bottom -= oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.left += ox;
            }
        }
    }

    @SuppressWarnings("UnnecessaryLocalVariable")
    private void moveHandleRB(float diffX, float diffY) {
        if (mCropMode == CropMode.FREE) {
            mFrameRect.right += diffX;
            mFrameRect.bottom += diffY;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
            }
            checkScaleBounds();
        } else {
            float dx = diffX;
            float dy = diffX * getRatioY() / getRatioX();
            mFrameRect.right += dx;
            mFrameRect.bottom += dy;
            if (isWidthTooSmall()) {
                float offsetX = mMinFrameSize - getFrameW();
                mFrameRect.right += offsetX;
                float offsetY = offsetX * getRatioY() / getRatioX();
                mFrameRect.bottom += offsetY;
            }
            if (isHeightTooSmall()) {
                float offsetY = mMinFrameSize - getFrameH();
                mFrameRect.bottom += offsetY;
                float offsetX = offsetY * getRatioX() / getRatioY();
                mFrameRect.right += offsetX;
            }
            float ox, oy;
            if (!isInsideHorizontal(mFrameRect.right)) {
                ox = mFrameRect.right - mImageRect.right;
                mFrameRect.right -= ox;
                oy = ox * getRatioY() / getRatioX();
                mFrameRect.bottom -= oy;
            }
            if (!isInsideVertical(mFrameRect.bottom)) {
                oy = mFrameRect.bottom - mImageRect.bottom;
                mFrameRect.bottom -= oy;
                ox = oy * getRatioX() / getRatioY();
                mFrameRect.right -= ox;
            }
        }
    }

    // Frame position correction ///////////////////////////////////////////////////////////////////

    private void checkScaleBounds() {
        float lDiff = mFrameRect.left - mImageRect.left;
        float rDiff = mFrameRect.right - mImageRect.right;
        float tDiff = mFrameRect.top - mImageRect.top;
        float bDiff = mFrameRect.bottom - mImageRect.bottom;

        if (lDiff < 0) {
            mFrameRect.left -= lDiff;
        }
        if (rDiff > 0) {
            mFrameRect.right -= rDiff;
        }
        if (tDiff < 0) {
            mFrameRect.top -= tDiff;
        }
        if (bDiff > 0) {
            mFrameRect.bottom -= bDiff;
        }
    }

    private void checkMoveBounds() {
        float diff = mFrameRect.left - mImageRect.left;
        if (diff < 0) {
            mFrameRect.left -= diff;
            mFrameRect.right -= diff;
        }
        diff = mFrameRect.right - mImageRect.right;
        if (diff > 0) {
            mFrameRect.left -= diff;
            mFrameRect.right -= diff;
        }
        diff = mFrameRect.top - mImageRect.top;
        if (diff < 0) {
            mFrameRect.top -= diff;
            mFrameRect.bottom -= diff;
        }
        diff = mFrameRect.bottom - mImageRect.bottom;
        if (diff > 0) {
            mFrameRect.top -= diff;
            mFrameRect.bottom -= diff;
        }
    }

    private boolean isInsideHorizontal(float x) {
        return mImageRect.left <= x && mImageRect.right >= x;
    }

    private boolean isInsideVertical(float y) {
        return mImageRect.top <= y && mImageRect.bottom >= y;
    }

    private boolean isWidthTooSmall() {
        return getFrameW() < mMinFrameSize;
    }

    private boolean isHeightTooSmall() {
        return getFrameH() < mMinFrameSize;
    }

    // Frame aspect ratio correction ///////////////////////////////////////////////////////////////

    private void recalculateFrameRect(int durationMillis) {
        if (mImageRect == null) return;
        if (mIsAnimating) {
            getAnimator().cancelAnimation();
        }
        final RectF currentRect = new RectF(mFrameRect);
        final RectF newRect = calcFrameRect(mImageRect);
        final float diffL = newRect.left - currentRect.left;
        final float diffT = newRect.top - currentRect.top;
        final float diffR = newRect.right - currentRect.right;
        final float diffB = newRect.bottom - currentRect.bottom;
        if (mIsAnimationEnabled) {
            SimpleValueAnimator animator = getAnimator();
            animator.addAnimatorListener(new SimpleValueAnimatorListener() {
                @Override
                public void onAnimationStarted() {
                    mIsAnimating = true;
                }

                @Override
                public void onAnimationUpdated(float scale) {
                    mFrameRect = new RectF(currentRect.left + diffL * scale, currentRect.top + diffT * scale,
                            currentRect.right + diffR * scale, currentRect.bottom + diffB * scale);
                    invalidate();
                }

                @Override
                public void onAnimationFinished() {
                    mFrameRect = newRect;
                    invalidate();
                    mIsAnimating = false;
                }
            });
            animator.startAnimation(durationMillis);
        } else {
            mFrameRect = calcFrameRect(mImageRect);
            invalidate();
        }
    }

    private float getRatioX(float w) {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.width();
            case FREE:
                return w;
            case RATIO_4_3:
                return 4;
            case RATIO_3_4:
                return 3;
            case RATIO_16_9:
                return 16;
            case RATIO_9_16:
                return 9;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.x;
            default:
                return w;
        }
    }

    private float getRatioY(float h) {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.height();
            case FREE:
                return h;
            case RATIO_4_3:
                return 3;
            case RATIO_3_4:
                return 4;
            case RATIO_16_9:
                return 9;
            case RATIO_9_16:
                return 16;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.y;
            default:
                return h;
        }
    }

    private float getRatioX() {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.width();
            case RATIO_4_3:
                return 4;
            case RATIO_3_4:
                return 3;
            case RATIO_16_9:
                return 16;
            case RATIO_9_16:
                return 9;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.x;
            default:
                return 1;
        }
    }

    private float getRatioY() {
        switch (mCropMode) {
            case FIT_IMAGE:
                return mImageRect.height();
            case RATIO_4_3:
                return 3;
            case RATIO_3_4:
                return 4;
            case RATIO_16_9:
                return 9;
            case RATIO_9_16:
                return 16;
            case SQUARE:
            case CIRCLE:
            case CIRCLE_SQUARE:
                return 1;
            case CUSTOM:
                return mCustomRatio.y;
            default:
                return 1;
        }
    }

    // Utility /////////////////////////////////////////////////////////////////////////////////////

    private float getDensity() {
        return DisplayManager.getInstance().getDefaultDisplay(getContext()).get().getAttributes().densityPixels;
    }

    private float sq(float value) {
        return value * value;
    }

    private float constrain(float val, float min, float max, float defaultVal) {
        if (val < min || val > max) return defaultVal;
        return val;
    }

    private void postErrorOnMainThread(final Callback callback, final Throwable e) {
        if (callback == null) return;
        if (EventRunner.current() == EventRunner.getMainEventRunner()) {
            callback.onError(e);
        } else {
            mHandler.postSyncTask(new Runnable() {
                @Override
                public void run() {
                    callback.onError(e);
                }
            });
        }
    }

    private PixelMap getBitmap() {
        return getPixelMap();
    }

    private float getRotatedWidth(float angle) {
        return getRotatedWidth(angle, mImgWidth, mImgHeight);
    }

    private float getRotatedWidth(float angle, float width, float height) {
        return angle % 180 == 0 ? width : height;
    }

    private float getRotatedHeight(float angle) {
        return getRotatedHeight(angle, mImgWidth, mImgHeight);
    }

    private float getRotatedHeight(float angle, float width, float height) {
        return angle % 180 == 0 ? height : width;
    }

    private PixelMap getRotatedBitmap(PixelMap bitmap) {
//        Matrix rotateMatrix = new Matrix();
//        rotateMatrix.setRotate(mAngle, bitmap.getImageInfo().size.width / 2, bitmap.getImageInfo().size.height / 2);
        PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
        initializationOptions.size.width = bitmap.getImageInfo().size.width;
        initializationOptions.size.height = bitmap.getImageInfo().size.height;
        return PixelMap.create(bitmap, initializationOptions);
    }

    // Animation ///////////////////////////////////////////////////////////////////////////////////

    private SimpleValueAnimator getAnimator() {
        setupAnimatorIfNeeded();
        return mAnimator;
    }

    private void setupAnimatorIfNeeded() {
        if (mAnimator == null) {
            mAnimator = new ValueAnimatorV14();
        }
    }

    // Cropping ////////////////////////////////////////////////////////////////////////////////////

    /**
     * 获取裁剪图片后的PixelMap
     *
     * @return
     * @throws Exception
     */
    private PixelMap getCroppedBitmap() throws Exception {
        PixelMap cropped = null;
        final int originalImageWidth = getWidth();
        final int originalImageHeight = getHeight();
        Rect cropRect = calcCropRect(originalImageWidth, originalImageHeight);
        if (mAngle != 0) {
            Matrix matrix = new Matrix();
            matrix.setRotate(-mAngle);
            RectF rotated = new RectF();
            matrix.mapRect(rotated, new RectF(cropRect));
            rotated.offset(rotated.left < 0 ? originalImageWidth : 0, rotated.top < 0 ? originalImageHeight : 0);
            cropRect = new Rect((int) rotated.left, (int) rotated.top, (int) rotated.right, (int) rotated.bottom);
        }
        float left = cropRect.left;
        float top = cropRect.top;
        int minX = Math.round(left);
        int minY = Math.round(top);

        int cropWidth = Math.round(cropRect.getWidth());
        int cropHeight = Math.round(cropRect.getHeight());

        ohos.media.image.common.Rect cropperRect = new ohos.media.image.common.Rect(minX , minY + 500, cropWidth + 120, cropHeight);
        PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
        initializationOptions.pixelFormat = PixelFormat.ARGB_8888;
        initializationOptions.size = new Size(cropWidth, cropHeight);
        cropped = PixelMap.create(getBitmap(), cropperRect, initializationOptions);
        if (mAngle != 0 && cropped != null) {
            PixelMap rotated = getRotatedBitmap(cropped);
            if (cropped != getBitmap() && cropped != rotated) {
//                cropped.release();
            }
            cropped = rotated;
        }
        return cropped;
    }

    /**
     * 计算裁剪
     *
     * @param originalImageWidth
     * @param originalImageHeight
     * @return
     */
    private Rect calcCropRect(int originalImageWidth, int originalImageHeight) {
        mAngle = 0;
        float scaleToOriginal =
                getRotatedWidth(mAngle, originalImageWidth, originalImageHeight) / mImageRect.width();
        float offsetX = mImageRect.left * scaleToOriginal;
        float offsetY = mImageRect.top * scaleToOriginal;
        int left = Math.round(mFrameRect.left * scaleToOriginal - offsetX);
        int top = Math.round(mFrameRect.top * scaleToOriginal - offsetY);
        int right = Math.round(mFrameRect.right * scaleToOriginal - offsetX);
        int bottom = Math.round(mFrameRect.bottom * scaleToOriginal - offsetY);
        int imageW = Math.round(getRotatedWidth(mAngle, originalImageWidth, originalImageHeight));
        int imageH = Math.round(getRotatedHeight(mAngle, originalImageWidth, originalImageHeight));
        return new Rect(Math.max(left, 0), Math.max(top, 0), Math.min(right, imageW),
                Math.min(bottom, imageH));
    }

    /**
     * 缩放位图
     *
     * @param cropped
     * @return
     */
    private PixelMap scaleBitmapIfNeeded(PixelMap cropped) {

        int width = cropped.getImageInfo().size.width;
        int height = cropped.getImageInfo().size.height;
        int outWidth = 0;
        int outHeight = 0;
        float imageRatio = getRatioX(mFrameRect.width()) / getRatioY(mFrameRect.height());

        if (mOutputWidth > 0) {
            outWidth = mOutputWidth;
            outHeight = Math.round(mOutputWidth / imageRatio);
        } else if (mOutputHeight > 0) {
            outHeight = mOutputHeight;
            outWidth = Math.round(mOutputHeight * imageRatio);
        } else {
            if (mOutputMaxWidth > 0 && mOutputMaxHeight > 0 && (width > mOutputMaxWidth
                    || height > mOutputMaxHeight)) {
                float maxRatio = (float) mOutputMaxWidth / (float) mOutputMaxHeight;
                if (maxRatio >= imageRatio) {
                    outHeight = mOutputMaxHeight;
                    outWidth = Math.round((float) mOutputMaxHeight * imageRatio);
                } else {
                    outWidth = mOutputMaxWidth;
                    outHeight = Math.round((float) mOutputMaxWidth / imageRatio);
                }
            }
        }

        if (outWidth > 0 && outHeight > 0) {
            PixelMap scaled = Utils.getScaledBitmap(cropped, outWidth, outHeight);
            if (cropped != getBitmap() && cropped != scaled) {
//                cropped.release();
            }
            cropped = scaled;
        }
        return cropped;
    }

    // File save ///////////////////////////////////////////////////////////////////////////////////

    /**
     * 保存图片
     *
     * @param bitmap
     * @param uri
     * @return
     * @throws Exception
     * @throws IllegalStateException
     */
    private Uri saveImage(PixelMap bitmap, final Uri uri) throws Exception, IllegalStateException {
        mSaveUri = uri;
        if (mSaveUri == null) {
            throw new IllegalStateException("Save uri must not be null.");
        }

        OutputStream outputStream = null;
        try {
            File file = new File(uri.getDecodedPath());
            if (!file.exists()) {
                file.createNewFile();
            }
            outputStream = new FileOutputStream(file.getPath());
            ImagePacker imagePacker = ImagePacker.create();
            ImagePacker.PackingOptions packingOptions = new ImagePacker.PackingOptions();
            packingOptions.quality = 90;
            packingOptions.format = "image/jpeg";
            imagePacker.initializePacking(outputStream, packingOptions);
            imagePacker.addImage(bitmap);
            imagePacker.finalizePacking();
            outputStream.close();
            outputStream.flush();
            imagePacker.release();
            Utils.copyExifInfo(getContext(), mSourceUri, uri, bitmap.getImageInfo().size.width, bitmap.getImageInfo().size.height);
            Utils.updateGalleryInfo(getContext(), uri);
            return uri;
        } finally {
            Utils.closeQuietly(outputStream);
        }
    }

    // Public methods //////////////////////////////////////////////////////////////////////////////

    /**
     * Get source image bitmap
     *
     * @return src bitmap
     */
    public PixelMap getImageBitmap() {
        return getBitmap();
    }


    /**
     * Set image drawable.
     *
     * @param element source image element
     */
    @Override
    public void setImageElement(Element element) {
        setImageSrc(element);
    }

    @Override
    public void setImageAndDecodeBounds(int resId) {
        try {
            ResourceManager resourceManager = getResourceManager();
            setImageSrcByResource(resourceManager.getResource(resId));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void setPixelMap(PixelMap pixelMap) {
        super.setPixelMap(pixelMap);
        resetImageInfo();
        updateLayout();
    }

    private void setImageDrawableInternal(Element drawable) {
        updateLayout();
    }


    private void updateLayout() {
        Element d = getImageElement();
        if (d != null) {
            setupLayout(mViewWidth, mViewHeight);
        }
    }

    private void resetImageInfo() {
        if (mIsLoading.get()) return;
        mSourceUri = null;
        mSaveUri = null;
        mInputImageWidth = 0;
        mInputImageHeight = 0;
        mOutputImageWidth = 0;
        mOutputImageHeight = 0;
        mAngle = mExifRotation;
    }

    /**
     * Load image from Uri.
     * This method is deprecated. Use loadAsync(Uri, LoadCallback) instead.
     *
     * @param sourceUri Image Uri
     * @param callback  Callback
     * @see #loadAsync(Uri, LoadCallback)
     */
    public void startLoad(final Uri sourceUri, final LoadCallback callback) {
        loadAsync(sourceUri, callback);
    }

    /**
     * Load image from Uri.
     *
     * @param sourceUri Image Uri
     * @param callback  Callback
     * @see #loadAsync(Uri, boolean, RectF, LoadCallback)
     */
    public void loadAsync(final Uri sourceUri, final LoadCallback callback) {
        loadAsync(sourceUri, false, null, callback);
    }


    public void loadAsync(final Uri sourceUri, final boolean useThumbnail,
                          final RectF initialFrameRect, final LoadCallback callback) {

        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mIsLoading.set(true);
                    mSourceUri = sourceUri;
                    mInitialFrameRect = initialFrameRect;
                    if (useThumbnail) {
                        applyThumbnail(sourceUri);
                    }
                    final PixelMap sampled = getImage(sourceUri);
                    mHandler.postSyncTask(() -> {
                        mAngle = mExifRotation;
                        getContext().getUITaskDispatcher().asyncDispatch(() -> setPixelMap(sampled));
                        if (callback != null) callback.onSuccess(sampled);
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(callback, e);
                } finally {
                    mIsLoading.set(false);
                }
            }
        });
    }


    public Observable loadAsCompletable(final Uri sourceUri) {
        return loadAsCompletable(sourceUri, false, null);
    }


    public Observable loadAsCompletable(final Uri sourceUri, final boolean useThumbnail,
                                        final RectF initialFrameRect) {
        Observable<Integer> observable = Observable.create(emitter -> {
            mInitialFrameRect = initialFrameRect;
            mSourceUri = sourceUri;

            if (useThumbnail) {
                applyThumbnail(sourceUri);
            }

            final PixelMap sampled = getImage(sourceUri);
            mHandler.postSyncTask(new Runnable() {
                @Override
                public void run() {
                    mAngle = mExifRotation;
                    getContext().getUITaskDispatcher().asyncDispatch(() -> setPixelMap(sampled));
                    emitter.onComplete();
                }
            });
        });
        observable.subscribe(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) throws Exception {
                mIsLoading.set(false);
            }
        });
        return observable;
    }

    /**
     * Load image from Uri with Builder Pattern
     *
     * @param sourceUri Image Uri
     * @return Builder
     */
    public LoadRequest load(Uri sourceUri) {
        return new LoadRequest(this, sourceUri);
    }

    private void applyThumbnail(Uri sourceUri) {
        final PixelMap thumb = getThumbnail(sourceUri);
        if (thumb == null)
            mHandler.postSyncTask(new Runnable() {
                @Override
                public void run() {
                    mAngle = mExifRotation;
                    setImageDrawableInternal(new PixelMapElement(thumb));
                }
            });
    }

    private PixelMap getImage(Uri sourceUri) {

        if (sourceUri == null) {
            throw new IllegalStateException("Source Uri must not be null.");
        }

        mExifRotation = Utils.getExifOrientation(getContext(), mSourceUri);
        final PixelMap sampledBitmap =
                Utils.decodeSampledBitmapFromUri(getContext(), mSourceUri, 0);
        return sampledBitmap;
    }

    private PixelMap getThumbnail(Uri sourceUri) {

        if (sourceUri == null) {
            throw new IllegalStateException("Source Uri must not be null.");
        }

        mExifRotation = Utils.getExifOrientation(getContext(), mSourceUri);
        int requestSize = (int) (Math.max(mViewWidth, mViewHeight) * 0.1f);
        if (requestSize == 0) return null;

        final PixelMap sampledBitmap =
                Utils.decodeSampledBitmapFromUri(getContext(), mSourceUri, requestSize);
        mInputImageWidth = Utils.sInputImageWidth;
        mInputImageHeight = Utils.sInputImageHeight;
        return sampledBitmap;
    }

    /**
     * Rotate image
     *
     * @param degrees        rotation angle
     * @param durationMillis animation duration in milliseconds
     */
    public void rotateImage(RotateDegrees degrees, int durationMillis) {
        if (mIsRotating) {
            getAnimator().cancelAnimation();
        }

        final float currentAngle = mAngle;
        final float newAngle = (mAngle + degrees.getValue());
        final float angleDiff = newAngle - currentAngle;
        final float currentScale = mScale;
        final float newScale = calcScale(mViewWidth, mViewHeight, newAngle);

        if (mIsAnimationEnabled) {
            final float scaleDiff = newScale - currentScale;
            SimpleValueAnimator animator = getAnimator();
            animator.addAnimatorListener(new SimpleValueAnimatorListener() {
                @Override
                public void onAnimationStarted() {
                    mIsRotating = true;
                }

                @Override
                public void onAnimationUpdated(float scale) {
                    mAngle = currentAngle + angleDiff * scale;
                    mScale = currentScale + scaleDiff * scale;
                    setMatrix();
                    invalidate();
                    mIsRotating = false;
                }

                @Override
                public void onAnimationFinished() {
                    mAngle = newAngle % 360;
                    mScale = newScale;
                    mInitialFrameRect = null;
                    setupLayout(mViewWidth, mViewHeight);
                    mIsRotating = false;
                }
            });
            animator.startAnimation(durationMillis);
        } else {
            mAngle = newAngle % 360;
            mScale = newScale;
            setupLayout(mViewWidth, mViewHeight);
        }
    }

    /**
     * Rotate image
     *
     * @param degrees rotation angle
     */
    public void rotateImage(RotateDegrees degrees) {
        rotateImage(degrees, mAnimationDurationMillis);
    }


    /**
     * Crop the square image in a circular
     *
     * @param square image bitmap
     * @return circular image bitmap
     */
    public PixelMap getCircularBitmap(PixelMap square) {
        if (square == null) return null;
        PixelMap.InitializationOptions initializationOptions = new PixelMap.InitializationOptions();
        initializationOptions.size = square.getImageInfo().size;
        initializationOptions.pixelFormat = PixelFormat.ARGB_8888;
        PixelMap output = PixelMap.create(initializationOptions);
        final Rect rect = new Rect(0, 0, square.getImageInfo().size.width, square.getImageInfo().size.height);
        Canvas canvas = new Canvas(new Texture(output));
        int halfWidth = square.getImageInfo().size.width / 2;
        int halfHeight = square.getImageInfo().size.height / 2;

        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        paint.setFilterBitmap(true);

        canvas.drawCircle(halfWidth, halfHeight, Math.min(halfWidth, halfHeight), paint);
        paint.setBlendMode(BlendMode.SRC_IN);
        RectFloat rectFloat = new RectFloat();
        rectFloat.left = rect.left;
        rectFloat.right = rect.right;
        rectFloat.top = rect.top;
        rectFloat.bottom = rect.bottom;
        canvas.drawPixelMapHolderRect(new PixelMapHolder(square), rectFloat, rectFloat, paint);
        return output;
    }

    /**
     * Crop image
     * This method is separated to #crop(Uri) and #save(Bitmap)
     * Use #crop(Uri) and #save(Bitmap)
     *
     * @param saveUri      Uri for saving the cropped image
     * @param cropCallback Callback for cropping the image
     * @param saveCallback Callback for saving the image
     * @see #crop(Uri)
     */
    public void startCrop(final Uri saveUri, final CropCallback cropCallback,
                          final SaveCallback saveCallback) {

        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                PixelMap croppedImage = null;

                try {
                    mIsCropping.set(true);

                    croppedImage = cropImage();

                    final PixelMap cropped = croppedImage;
                    mHandler.postSyncTask(new Runnable() {
                        @Override
                        public void run() {
                            if (cropCallback != null) cropCallback.onSuccess(cropped);
                            if (mIsDebug) invalidate();
                        }
                    });
                    saveImage(croppedImage, saveUri);
                    mHandler.postSyncTask(new Runnable() {
                        @Override
                        public void run() {
                            if (saveCallback != null) saveCallback.onSuccess(saveUri);
                        }
                    });
                } catch (Exception e) {
                    if (croppedImage == null) {
                        postErrorOnMainThread(cropCallback, e);
                    } else {
                        postErrorOnMainThread(saveCallback, e);
                    }
                } finally {
                    mIsCropping.set(false);
                }
            }
        });
    }

    /**
     * Crop image
     *
     * @param sourceUri    Uri for cropping(If null, the Uri set in loadAsync() is used)
     * @param cropCallback Callback for cropping the image
     * @see #crop(Uri)
     * @see #cropAsSingle(Uri)
     */
    public void cropAsync(final Uri sourceUri, final CropCallback cropCallback) {
        mExecutor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    mIsCropping.set(true);

                    if (sourceUri != null) mSourceUri = sourceUri;

                    final PixelMap cropped = cropImage();
                    mHandler.postSyncTask(new Runnable() {
                        @Override
                        public void run() {
                            if (cropCallback != null) cropCallback.onSuccess(cropped);
                            if (mIsDebug) invalidate();
                        }
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(cropCallback, e);
                } finally {
                    mIsCropping.set(false);
                }
            }
        });
    }

    public void cropAsync(final CropCallback cropCallback) {
        cropAsync(null, cropCallback);
    }

    /**
     * Crop image with RxJava2
     *
     * @param sourceUri Uri for cropping(If null, the Uri set in loadAsSingle() is used)
     * @return Single of cropping image
     */
    public Single<PixelMap> cropAsSingle(final Uri sourceUri) {
        return Single.fromCallable(new Callable<PixelMap>() {

            @Override
            public PixelMap call() throws Exception {
                if (sourceUri != null) mSourceUri = sourceUri;
                return cropImage();
            }
        }).doOnSubscribe(new Consumer<Disposable>() {
            @Override
            public void accept(@NonNull Disposable disposable) throws Exception {
                mIsCropping.set(true);
            }
        }).doFinally(new Action() {
            @Override
            public void run() throws Exception {
                mIsCropping.set(false);
            }
        });
    }

    public Single<PixelMap> cropAsSingle() {
        return cropAsSingle(null);
    }

    /**
     * Crop image with Builder Pattern
     *
     * @param sourceUri Uri for cropping(If null, the Uri set in loadAsSingle() is used)
     * @return Builder
     */
    public CropRequest crop(Uri sourceUri) {
        return new CropRequest(this, sourceUri);
    }

    /**
     * Save image
     *
     * @param saveUri      Uri for saving the cropped image
     * @param image        Image for saving
     * @param saveCallback Callback for saving the image
     */
    public void saveAsync(final Uri saveUri, final PixelMap image, final SaveCallback saveCallback) {
        mExecutor.submit(new Runnable() {

            @Override
            public void run() {
                try {
                    mIsSaving.set(true);
                    saveImage(image, saveUri);
                    mHandler.postSyncTask(new Runnable() {
                        @Override
                        public void run() {
                            if (saveCallback != null) saveCallback.onSuccess(saveUri);
                        }
                    });
                } catch (Exception e) {
                    postErrorOnMainThread(saveCallback, e);
                } finally {
                    mIsSaving.set(false);
                }
            }
        });
    }

    /**
     * Save image with RxJava2
     *
     * @param bitmap  Bitmap for saving
     * @param saveUri Uri for saving the cropped image
     * @return Single of saving image
     */
    public Single<Uri> saveAsSingle(final PixelMap bitmap, final Uri saveUri) {
        return Single.fromCallable(new Callable<Uri>() {

            @Override
            public Uri call() throws Exception {
                return saveImage(bitmap, saveUri);
            }
        }).doOnSubscribe(new Consumer<Disposable>() {
            @Override
            public void accept(@NonNull Disposable disposable) throws Exception {
                mIsSaving.set(true);
            }
        }).doFinally(new Action() {
            @Override
            public void run() throws Exception {
                mIsSaving.set(false);
            }
        });
    }

    /**
     * Save image with Builder Pattern
     *
     * @param bitmap image for saving
     * @return Builder
     */
    public SaveRequest save(PixelMap bitmap) {
        return new SaveRequest(this, bitmap);
    }

    private PixelMap cropImage() throws Exception {
        PixelMap cropped;
        cropped = getCroppedBitmap();
        if (mCropMode == CropMode.CIRCLE) {
            PixelMap circle = getCircularBitmap(cropped);
            if (cropped != getBitmap() && cropped != null) {
//                cropped.release();
            }
            cropped = circle;
        }
        cropped = scaleBitmapIfNeeded(cropped);
        mOutputImageWidth = cropped.getImageInfo().size.width;
        mOutputImageHeight = cropped.getImageInfo().size.height;

        return cropped;
    }

    /**
     * Get frame position relative to the source bitmap.
     *
     * @return getCroppedBitmap area boundaries.
     * @see #load(Uri)
     * @see #loadAsync(Uri, boolean, RectF, LoadCallback)
     * @see #loadAsCompletable(Uri, boolean, RectF)
     */
    public RectF getActualCropRect() {
        if (mImageRect == null) return null;
        float offsetX = (mImageRect.left / mScale);
        float offsetY = (mImageRect.top / mScale);
        float l = (mFrameRect.left / mScale) - offsetX;
        float t = (mFrameRect.top / mScale) - offsetY;
        float r = (mFrameRect.right / mScale) - offsetX;
        float b = (mFrameRect.bottom / mScale) - offsetY;
        l = Math.max(0, l);
        t = Math.max(0, t);
        r = Math.min(mImageRect.right / mScale, r);
        b = Math.min(mImageRect.bottom / mScale, b);
        return new RectF(l, t, r, b);
    }

    private RectF applyInitialFrameRect(RectF initialFrameRect) {
        RectF frameRect = new RectF();
        frameRect.set(initialFrameRect.left * mScale, initialFrameRect.top * mScale,
                initialFrameRect.right * mScale, initialFrameRect.bottom * mScale);
        frameRect.offset(mImageRect.left, mImageRect.top);
        float l = Math.max(mImageRect.left, frameRect.left);
        float t = Math.max(mImageRect.top, frameRect.top);
        float r = Math.min(mImageRect.right, frameRect.right);
        float b = Math.min(mImageRect.bottom, frameRect.bottom);
        frameRect.set(l, t, r, b);
        return frameRect;
    }

    /**
     * Set getCroppedBitmap mode
     *
     * @param mode           getCroppedBitmap mode
     * @param durationMillis animation duration in milliseconds
     */
    public void setCropMode(CropMode mode, int durationMillis) {
        if (mode == CropMode.CUSTOM) {
            setCustomRatio(1, 1);
        } else {
            mCropMode = mode;
            recalculateFrameRect(durationMillis);
        }
    }

    /**
     * Set getCroppedBitmap mode
     *
     * @param mode getCroppedBitmap mode
     */
    public void setCropMode(CropMode mode) {
        setCropMode(mode, mAnimationDurationMillis);
    }

    /**
     * Set custom aspect ratio to getCroppedBitmap frame
     *
     * @param ratioX         ratio x
     * @param ratioY         ratio y
     * @param durationMillis animation duration in milliseconds
     */
    public void setCustomRatio(int ratioX, int ratioY, int durationMillis) {
        if (ratioX == 0 || ratioY == 0) return;
        mCropMode = CropMode.CUSTOM;
        mCustomRatio = new PointF(ratioX, ratioY);
        recalculateFrameRect(durationMillis);
    }

    /**
     * Set custom aspect ratio to getCroppedBitmap frame
     *
     * @param ratioX ratio x
     * @param ratioY ratio y
     */
    public void setCustomRatio(int ratioX, int ratioY) {
        setCustomRatio(ratioX, ratioY, mAnimationDurationMillis);
    }

    /**
     * Set image overlay color
     *
     * @param overlayColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setOverlayColor(int overlayColor) {
        this.mOverlayColor = overlayColor;
        invalidate();
    }

    /**
     * Set getCroppedBitmap frame color
     *
     * @param frameColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setFrameColor(int frameColor) {
        this.mFrameColor = frameColor;
        invalidate();
    }

    /**
     * Set handle color
     *
     * @param handleColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setHandleColor(int handleColor) {
        this.mHandleColor = handleColor;
        invalidate();
    }

    /**
     * Set guide color
     *
     * @param guideColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setGuideColor(int guideColor) {
        this.mGuideColor = guideColor;
        invalidate();
    }

    /**
     * Set view background color
     *
     * @param bgColor color resId or color int(ex. 0xFFFFFFFF)
     */
    public void setBackgroundColor(int bgColor) {
        this.mBackgroundColor = bgColor;
        invalidate();
    }

    /**
     * Set getCroppedBitmap frame minimum size in density-independent pixels.
     *
     * @param minDp getCroppedBitmap frame minimum size in density-independent pixels
     */
    public void setMinFrameSizeInDp(int minDp) {
        mMinFrameSize = minDp * getDensity();
    }

    /**
     * Set getCroppedBitmap frame minimum size in pixels.
     *
     * @param minPx getCroppedBitmap frame minimum size in pixels
     */
    public void setMinFrameSizeInPx(int minPx) {
        mMinFrameSize = minPx;
    }

    /**
     * Set handle radius in density-independent pixels.
     *
     * @param handleDp handle radius in density-independent pixels
     */
    public void setHandleSizeInDp(int handleDp) {
        mHandleSize = (int) (handleDp * getDensity());
    }

    /**
     * Set getCroppedBitmap frame handle touch padding(touch area) in density-independent pixels.
     * <p>
     * handle touch area : a circle of radius R.(R = handle size + touch padding)
     *
     * @param paddingDp getCroppedBitmap frame handle touch padding(touch area) in
     *                  density-independent
     *                  pixels
     */
    public void setTouchPaddingInDp(int paddingDp) {
        mTouchPadding = (int) (paddingDp * getDensity());
    }

    /**
     * Set guideline show mode.
     * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH)
     *
     * @param mode guideline show mode
     */
    public void setGuideShowMode(ShowMode mode) {
        mGuideShowMode = mode;
        switch (mode) {
            case SHOW_ALWAYS:
                mShowGuide = true;
                break;
            case NOT_SHOW:
            case SHOW_ON_TOUCH:
                mShowGuide = false;
                break;
        }
        invalidate();
    }

    /**
     * Set handle show mode.
     * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH)
     *
     * @param mode handle show mode
     */
    public void setHandleShowMode(ShowMode mode) {
        mHandleShowMode = mode;
        switch (mode) {
            case SHOW_ALWAYS:
                mShowHandle = true;
                break;
            case NOT_SHOW:
            case SHOW_ON_TOUCH:
                mShowHandle = false;
                break;
        }
        invalidate();
    }

    /**
     * Set frame stroke weight in density-independent pixels.
     *
     * @param weightDp frame stroke weight in density-independent pixels.
     */
    public void setFrameStrokeWeightInDp(int weightDp) {
        mFrameStrokeWeight = weightDp * getDensity();
        invalidate();
    }

    /**
     * Set guideline stroke weight in density-independent pixels.
     *
     * @param weightDp guideline stroke weight in density-independent pixels.
     */
    public void setGuideStrokeWeightInDp(int weightDp) {
        mGuideStrokeWeight = weightDp * getDensity();
        invalidate();
    }

    /**
     * Set whether to show getCroppedBitmap frame.
     *
     * @param enabled should show getCroppedBitmap frame?
     */
    public void setCropEnabled(boolean enabled) {
        mIsCropEnabled = enabled;
        invalidate();
    }

    /**
     * Set locking the getCroppedBitmap frame.
     *
     * @param enabled should lock getCroppedBitmap frame?
     */
    @Override
    public void setEnabled(boolean enabled) {
        super.setEnabled(enabled);
        mIsEnabled = enabled;
    }

    /**
     * Set initial scale of the frame.(0.01 ~ 1.0)
     *
     * @param initialScale initial scale
     */
    public void setInitialFrameScale(float initialScale) {
        mInitialFrameScale = constrain(initialScale, 0.01f, 1.0f, DEFAULT_INITIAL_FRAME_SCALE);
    }

    /**
     * Set whether to animate
     *
     * @param enabled is animation enabled?
     */
    public void setAnimationEnabled(boolean enabled) {
        mIsAnimationEnabled = enabled;
    }

    /**
     * Set duration of animation
     *
     * @param durationMillis animation duration in milliseconds
     */
    public void setAnimationDuration(int durationMillis) {
        mAnimationDurationMillis = durationMillis;
    }

    /**
     * Set whether to show debug display
     *
     * @param debug is logging enabled
     */
    public void setDebug(boolean debug) {
        mIsDebug = debug;
        Logger.setEnabled(true);
        invalidate();
    }

    /**
     * Set whether to log exception
     *
     * @param enabled is logging enabled
     */
    public static void setLoggingEnabled(boolean enabled) {
        Logger.enabled = enabled;
    }

    /**
     * Set fixed width for output
     * (After cropping, the image is scaled to the specified size.)
     *
     * @param outputWidth output width
     */
    public void setOutputWidth(int outputWidth) {
        mOutputWidth = outputWidth;
        mOutputHeight = 0;
    }

    /**
     * Set fixed height for output
     * (After cropping, the image is scaled to the specified size.)
     *
     * @param outputHeight output height
     */
    public void setOutputHeight(int outputHeight) {
        mOutputHeight = outputHeight;
        mOutputWidth = 0;
    }

    /**
     * Set maximum size for output
     * (If cropped image size is larger than max size, the image is scaled to the smaller size.
     * If fixed output width/height has already set, these parameters are ignored.)
     *
     * @param maxWidth  max output width
     * @param maxHeight max output height
     */
    public void setOutputMaxSize(int maxWidth, int maxHeight) {
        mOutputMaxWidth = maxWidth;
        mOutputMaxHeight = maxHeight;
    }


    /**
     * Set compress quality for output
     *
     * @param quality compress quality(0-100: 100 is default.)
     */
    public void setCompressQuality(int quality) {
        mCompressQuality = quality;
    }

    /**
     * Set whether to show handle shadows
     *
     * @param handleShadowEnabled should show handle shadows?
     */
    public void setHandleShadowEnabled(boolean handleShadowEnabled) {
        mIsHandleShadowEnabled = handleShadowEnabled;
    }

    /**
     * cropping status
     *
     * @return is cropping process running
     */
    public boolean isCropping() {
        return mIsCropping.get();
    }

    /**
     * source uri
     *
     * @return source uri
     */
    public Uri getSourceUri() {
        return mSourceUri;
    }

    /**
     * save uri
     *
     * @return save uri
     */
    public Uri getSaveUri() {
        return mSaveUri;
    }

    /**
     * saving status
     *
     * @return is saving process running
     */
    public boolean isSaving() {
        return mIsSaving.get();
    }

    private void setScale(float mScale) {
        this.mScale = mScale;
    }

    private void setCenter(PointF mCenter) {
        this.mCenter = mCenter;
    }

    private float getFrameW() {
        return (mFrameRect.right - mFrameRect.left);
    }

    private float getFrameH() {
        return (mFrameRect.bottom - mFrameRect.top);
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (mIsInitialized) {
            setMatrix();
            PixelMap bm = getBitmap();
            if (bm == null) {
                bm = Utils.decodeSampledBitmapFromUri(getContext(), mSourceUri, 0);
            }
            if (bm != null) {
                drawCropFrame(canvas);
            }
        }
    }

    @Override
    public boolean onEstimateSize(int w, int h) {
        final int viewWidth = EstimateSpec.getSize(w);
        final int viewHeight = EstimateSpec.getSize(h);
        setEstimatedSize(viewWidth, viewHeight);
        mViewWidth = viewWidth - getPaddingLeft() - getPaddingRight();
        mViewHeight = viewHeight - getPaddingTop() - getPaddingBottom();
        setupLayout(mViewWidth, mViewHeight);
        return false;
    }


    // Enum ////////////////////////////////////////////////////////////////////////////////////////

    private enum TouchArea {
        OUT_OF_BOUNDS, CENTER, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM
    }

    public enum CropMode {
        FIT_IMAGE(0), RATIO_4_3(1), RATIO_3_4(2), SQUARE(3), RATIO_16_9(4), RATIO_9_16(5), FREE(
                6), CUSTOM(7), CIRCLE(8), CIRCLE_SQUARE(9);
        private final int ID;

        CropMode(final int id) {
            this.ID = id;
        }

        public int getId() {
            return ID;
        }
    }

    public enum ShowMode {
        SHOW_ALWAYS(1), SHOW_ON_TOUCH(2), NOT_SHOW(3);
        private final int ID;

        ShowMode(final int id) {
            this.ID = id;
        }

        public int getId() {
            return ID;
        }
    }

    public enum RotateDegrees {
        ROTATE_90D(90), ROTATE_180D(180), ROTATE_270D(270), ROTATE_M90D(-90), ROTATE_M180D(
                -180), ROTATE_M270D(-270);

        private final int VALUE;

        RotateDegrees(final int value) {
            this.VALUE = value;
        }

        public int getValue() {
            return VALUE;
        }
    }

    public void destroy() {
        if (null != mExecutor) {
            mExecutor.shutdown();
        }
    }

}
